Node多进程
Node 多进程开发入门
Node多进程核心是创建一个子进程,子进程依附在当前Node进程下面
核心类是 child_process
文档路径:http://nodejs.cn/api/child_process.html
子进程概念:是系统进行资源分配和调度的基本单位,是操作系统结构的基础
进程的概念只要有两点
- 进程是一个实体。每一个进程都有它的地址空间
- 进程是一个“执行中的程序”,存在嵌套关系
进程开线程,进程和线程的区别?
查看进程(os)
ps -ef 查看所有进程
Last login: Sun Feb 7 15:42:44 on ttys002
jolly@JollydeMacBook-Pro jolly-cli-dev % ps -ef
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 301220 ?? 14:35.92 /sbin/launchd
0 112 1 0 301220 ?? 0:56.66 /usr/sbin/syslogd
0 113 1 0 301220 ?? 2:26.60 /usr/libexec/UserEventAgent (System)
0 116 1 0 301220 ?? 0:13.29 /System/Library/PrivateFrameworks/Uninstall.framework/Resources/uninstalld
0 117 1 0 301220 ?? 0:54.79 /usr/libexec/kextd
0 118 1 0 301220 ?? 5:35.10 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Support/fseventsd
- UID 获得权限的用户的UID
- PID 进程的ID
ps -ef|grep PID可帅选出对应进程
- PPID 父进程的ID,体现进程的嵌套关系
vscode 启动 node 调试时的父子进程关系图。体现了进程嵌套

5-2 child_process异步方法使用教程(exec&execFile)
const cp = require('child_process');
child_process用法:
异步用法
- exec
- execFile
- fork
- spawn
同步用法
- execSync
- execFileSync
- spawnSync
异步
exec执行shell命令cp.exec('ls -la', function(err, stdout, stderr){
console.log('err: ', err); // 错误
console.log('stdout: ', stdout); // 正常运行输出的结果,后面会有个换行
console.log('stderr: ', stderr); // 异常输出的结果
});jolly@JollydeMacBook-Pro child_process % node index.js
err: null
stdout: total 8
drwxr-xr-x 3 jolly staff 96 2 7 16:58 .
drwxr-xr-x 14 jolly staff 448 2 7 16:58 ..
-rw-r--r-- 1 jolly staff 189 2 7 16:59 index.js
stderr:- 第二个参数是一个 option 对象
cwd设置执行的目录- timeout 设置执行命令超时的时间,默认是0(不超时)
- 第二个参数是一个 option 对象
execFile执行shell文件- 有四个参数
如果第一个参数不是路径,而是命令,那么通过
which 命令找到对应的文件cp.execFile('ls', ['-la'], function(err, stdout, stderr){
console.log('err: ', err);
console.log('stdout: ', stdout);
console.log('stderr: ', stderr);
});和上面
exec执行效果一样execFile第二个参数是,传入文件的参数和
exec的区别exec支持更复杂的shell命令:ls -la|grep node_modules但
execFile执行[-la|grep node_modules]会报错,不能执行成功。因为grep不是execFile执行文件ls的参数可以执行
.shell文件:将ls -al|grep node_modules写在.shell文件中chmod +x test.shell添加执行权限
test.shell文件当中的内容ls -la|grep node_modules
echo $1$1代表execFile传入的第一个参数
exec也能执行指定文件,但是不支持传入参数
spwan执行shell文件如果第一个参数不是路径,而是命令,那么通过
which 命令找到对应的文件const child = cp.spawn(path.resolve(__dirname, 'test.shell'), ['-la'], {
cwd: path.resolve('.')
})
child.stdout.on('data', function(chunk) {
console.log('stdout: ', chunk.toString()); //一次输出一个字符串
});
child.stderr.on('data', function(chunk) {
console.log('stderr: ', chunk.toString());
});
child.on('error', e => { // 监听错误
process.exit(1);
});
child.on('exit', e => {// 监听执行成功后的退出事件
});option参数
stdio选项用于配置在父进程和子进程之间建立的管道。值:pipe默认值,在子进程和父进程之间创建一个管道。ignore静默执行,不会收到反馈inherit将相应的 stdio 流传给父进程或从父进程传入。将输入、输出、错误,绑定到父进程的process.stdin、process.stdout和process.stderr上。直接能看到打印,还带动画(进度)信息 。
fork使用node执行命令- 一个参数:模块路径
- 和
require()的区别require加载的js模块是在主进程中执行的。fork测试在子进程中的执行的,执行的js文件process.pid会发生变化。
const child = cp.fork(path.resolve(__dirname, 'child.js'));- 适合执行耗时任务。
同步
执行简单 shell 命令
execSync执行shell命令const stdout = cp.execSync('ls -la|grep node_modules');
console.log(stdout.toString()); // stdout 是个 bufferexecFileSync执行shell文件const stdout = cp.execFileSync('ls', ['-la']);
console.log(stdout.toString()); // stdout 是个 bufferspawnSync执行shell文件const ret = cp.spawnSync('ls', ['-la']);
console.log(ret.stdout.toString()); // ret 是个 bufferexec、execFile、fork底层都是调用spawn的何时使用
spawn、exec、execFilespawn适合耗时任务(比如:npm install),需要不断日志。spawn逐条执行命令exec/execFile:开销比较小的任务。整个执行完后返回
使用
path.resovle('./','shell.js')时会报错,因为node的执行上下文,不一定是当前目录。要使用__dirname,比如path.resovle(__dirname,'shell.js')
5-4 child_process fork用法及父子进程通信机制讲解
- fork主要是使用node来执行我们的命令。
- fork会执行两个进程 主进程与子进程。
- fork的本质也是调用spawn。
const child = cp.fork(path.resolve(__dirname, 'child.js'));
// 向子进程发送消息
child.send('hello child process', () => {
// child.disconnect(); // 断开主、子进程直接的连接,否则,命令行将进入等待状态
});
// 接受子进程消息
child.on('mssage', msg => {
console.log('msg: ', msg);
child.disconnect();
});
console.log('main pid', process.pid);
child.js
console.log('child process');
console.log('child pid', process.pid);
// 接受主进程消息
process.on('message', msg => {
console.log('msg: ', msg);
});
// 向主进程发送消息
process.send('hello main process');
执行结果
jolly@JollydeMacBook-Pro child_process % node index.js
main pid 61868
child process
child pid 61869
msg: hello child process
msg: hello main process
注意:子进程向主进程发送消息,容易造成死循环
windows 子进程执行 node 命令
cp.spawn('cmd', ['/c', 'node', '-e', code]); // '/c' 表示静默执行
child_process异步源码解析
源码解读解决的问题
exec和execFile到底有什么区别- 为什么
exec/execFile/fork都是通过spawn实现的,spawn的作用到底是什么? - 为什么
spawn调用后没有回调,而exec/execFile能够回调? - 为什么
spawn调用后需要手动调用child.stdout.on('data', callback),这里的child.stdio/child.stderr到底是什么? - 为什么有
data/error/exit/close这么多种回调,它们的执行顺序到底是什么怎样的?
exec源码解读

exec和execFile的区别就是参数的区别在
execFile中调用spawn并且监听了stderr和stdout的data事件,执行事件处理函数,exec和execFile的回调函数就是这个事件处理函数。所以exec和execFile有回调函数执行
exec时,最后调用spawn规范后的参数
- 出现了
/bin/sh envPairs是环境变量
- 出现了
this._handle是实际的进程执行
child.spawn实际执行的是this._handle.spawn,执行后开启新进程exec执行后也是可以得到子进程对象的课程中调试源码时,
ls -la|grep node_modules报错,而直接在bash中运行不报错,是因为node有执行的环境,执行的环境不一样(执行时,所在路径不一样)- 统一执行环境后(没有node_modules文件夹),
bash中不会报错,但是没有结果。但调试还报错,是因为bash处理了错误。我们代码中输出了错误而已
- 统一执行环境后(没有node_modules文件夹),
shell 的使用
方法一:直接执行shell文件
/bin/sh test.shell方法二:直接执行
shell语句/bin/sh -c "ls -la"所以,没有
-c要指定文件路径shell命令ls -la===/bin/sh -c "ls -la"
exec 源码精读
对象的扩展运算符进行浅拷贝
// 等同于 {...Object(true)}
{...true} // {}
// 等同于 {...Object(undefined)}
{...undefined} // {}
// 等同于 {...Object(null)}
{...null} // {}
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
{ ...['a', 'b', 'c'] };
// {0: "a", 1: "b", 2: "c"}浅拷贝和深拷贝
- 浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 深拷贝:将一个对象从内存中完整的拷贝一份出来,包括属性指向的引用类型,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
注意:第二个参数传任何非
function类型,都会产生获得一个对象。function normalizeExecArgs(command, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
// 浅拷贝
options = { ...options }; // 将任意非 function 都将转化为参数
options.shell = typeof options.shell === 'string' ? options.shell : true; // 得到shell属性。
return {
file: command,
options: options, // options 至少有一个属性:shell
callback: callback
};
}
option.shell可以是一个字符串,用来执行命令的文件。默认值: Unix 上是'/bin/sh',Windows 上是process.env.ComSpecexecFile中首先对参数逐个判断,判断逻辑有点意思function execFile(file /* , args, options, callback */) {
let args = [];
let callback;
let options;
// 解析可选参数(第一个参数是 shell 文件路径),使用argument
let pos = 1;
if (pos < arguments.length && Array.isArray(arguments[pos])) { // 获得传入shell文件的参数
args = arguments[pos++];
} else if (pos < arguments.length && arguments[pos] == null) { // 第二个参数给 null 跳过第二个参数解析
pos++;
}
if (pos < arguments.length && typeof arguments[pos] === 'object') { // 参数是 Object 类型,认为是options
options = arguments[pos++];
} else if (pos < arguments.length && arguments[pos] == null) { // 参数值是 null,跳过
pos++;
}
if (pos < arguments.length && typeof arguments[pos] === 'function') { // 获得回调函数
callback = arguments[pos++];
}
if (!callback && pos < arguments.length && arguments[pos] != null) { // 经过以上步骤,传参了但没有解析到回调函数,报错。
throw new ERR_INVALID_ARG_VALUE('args', arguments[pos]);
}
...
}这样的参数解析,可以不用固定参数的顺序
查看
ERR_INVALID_ARG_VALUE的报错const cp = require('child_process');
cp.exec('ls -la', null,'sdds');要传入第二个参数时,才能看见第三个参数的报错。原因见”对象的扩展运算符“
olly@192 child_process % node index.js
child_process.js:202
throw new ERR_INVALID_ARG_VALUE('args', arguments[pos]);
^
TypeError [ERR_INVALID_ARG_VALUE]: The argument 'args' is invalid. Received 'sdds'
at Object.execFile (child_process.js:202:11)
at Object.exec (child_process.js:145:25)
at Object.<anonymous> (/Users/jolly/Desktop/imooc/child_process/index.js:4:4)
at Module._compile (internal/modules/cjs/loader.js:959:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
at Module.load (internal/modules/cjs/loader.js:815:32)
at Function.Module._load (internal/modules/cjs/loader.js:727:14)
at Function.Module.runMain (internal/modules/cjs/loader.js:1047:10)
at internal/main/run_main_module.js:17:11 {
code: 'ERR_INVALID_ARG_VALUE'
}
数组的浅拷贝
//args = args.slice(0)
var a = [1, 2, 3];
var b = a.slice(0); // b: [1, 2, 3]
a === b; // falsespawn中的命令拼接部分if (options.shell) {
const command = [file].concat(args).join(' '); // 拼接命令文件和传入的参数
// Set the shell, switches, and commands.
if (process.platform === 'win32') { // windows
if (typeof options.shell === 'string') // 自定义执行shell的文件
file = options.shell;
else
file = process.env.comspec || 'cmd.exe';
// '/d /s /c' is used only for cmd.exe.
if (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(file)) { // 匹配任意路径下的 cmd.exe。这里指定了 cmd.exe 的路径
args = ['/d', '/s', '/c', `"${command}"`]; // '/d /s /c' 仅用于 cmd.exe.
options.windowsVerbatimArguments = true; // options 中的 windowsVerbatimArguments 参数
} else {
args = ['-c', command];
}
} else {
if (typeof options.shell === 'string')
file = options.shell;
else if (process.platform === 'android') // 安卓系统
file = '/system/bin/sh';
else
file = '/bin/sh'; // 默认使用 '/bin/sh'
args = ['-c', command];
}
}spawn中的new ChildProcess()EventEmitter.call(this);之后,可以分发事件了。emit分发on监听
this._handle.onexit进程执行完之后回调child.spawn/ChildProcess.prototype.spawngetValidStdio()创建输入输出错误流输入流,子进程只有读权限
输出流,子进程只有写权限
new Pipe()创建socket通信,调用pipe_wrapipc建立进程间的双向通信,在fork时创建
循环建立父子进程 socket 通信
- socket 对象使用 on('data')监听
node_process回调调用流程

- Process 执行命令
child._handle.spawn(options)执行命令exitCode为0,表示执行成功,小于0表示失败
- 命令执行成功后,往”流“中写入信息,回调
onStreamRead方法读取流中信息 onStreamRead每读取完一条流中信息,调用一次onReadableStreamEndmaybeClose()中,判断所有socket关闭后,关闭子进程- 两条线:
- 子进程的执行线
- 流的读取线
事件处理函数执行顺序
const child = cp.execFile('ls -la', function(err, stdout, stderr){
console.log('callback start-----------');
console.log('err: ', err);
console.log('stdout: ', stdout);
console.log('stderr: ', stderr);
console.log('callback end-----------');
});
child.on('error', chunk => {
console.log('error! ', chunk);
})
child.stdout.on('data', chunk => {
console.log('stdout data: ', chunk);
});
child.stderr.on('data', chunk => {
console.log('stderr data: ', chunk);
});
child.stdout.on('close', chunk => {
console.log('stdout close');
});
child.stderr.on('close', chunk => {
console.log('stderr close');
});
child.on('exit', (exitCode, signalCode) => {
console.log('exit! ', exitCode, ' ', signalCode);
});
child.on('close', (exitCode, signalCode) => {
console.log('close! ', exitCode, ' ', signalCode);
});
jolly@192 child_process % node index.js
stdout data: total 24
drwxr-xr-x 6 jolly staff 192 2 10 15:11 .
drwxr-xr-x 14 jolly staff 448 2 7 16:58 ..
drwxr-xr-x 3 jolly staff 96 2 10 15:11 .vscode
-rw-r--r-- 1 jolly staff 225 2 7 21:10 child.js
-rw-r--r-- 1 jolly staff 1901 2 11 16:50 index.js
-rwxr-xr-x 1 jolly staff 15 2 7 20:19 test.shell
exit! 0 null
stderr close
callback start-----------
err: null
stdout: total 24
drwxr-xr-x 6 jolly staff 192 2 10 15:11 .
drwxr-xr-x 14 jolly staff 448 2 7 16:58 ..
drwxr-xr-x 3 jolly staff 96 2 10 15:11 .vscode
-rw-r--r-- 1 jolly staff 225 2 7 21:10 child.js
-rw-r--r-- 1 jolly staff 1901 2 11 16:50 index.js
-rwxr-xr-x 1 jolly staff 15 2 7 20:19 test.shell
stderr:
callback end-----------
close! 0 null
stdout close

exec 执行和回调脑图

颜色说明:
- 黄色:回调执行过程
- 紫色:广播事件
- 绿色:进程
error流程
关于 stderr
- 当命令执行失败,如
lss -ls时ChildProcess.prototype.spawn()中exitCode是 0,并不小于0
Buffer 对象的字符串解码器
在 fork 流程,setupChannel(child, ipc) 设置,其中涉及 Buffer 对象的字符串解码。
string_decoder 模块提供了一个 API,用一种能保护已编码的多字节 UTF-8 和 UTF-16 字符的方式将 Buffer 对象解码为字符串。基本用法
const { StringDecoder } = require('string_decoder');
const decoder = new StringDecoder('utf8');
const cent = Buffer.from([0xC2, 0xA2]);
console.log(decoder.write(cent));
const euro = Buffer.from([0xE2, 0x82, 0xAC]);
console.log(decoder.write(euro));
fork 源码解读

- 剩余部分见
exec执行脑图 - stdio
ipc通信:[0, 1, 2, 'ipc'] process.execPath拿到node路径- 重点
getValidStdio(stdio, false)- 执行
setupChannel(this, ipc),增强ipc功能,在父、子进程之间启动ipc:channel.readStart()new Control(channel)创建control对象,用于执行ipc的ref和unref方法- 有数据读取时,进入
channel.onread
- 执行
child.send()调用target.send()进行进程通信, 使用pipe进行数据传递- 在执行的
js文件中,process.send()也是使用target.send()进行通信
Node 多进程源码总结
- exec/execFile/spawn/fork的区别
exec: 原理是调用bin/shell -c执行我们传入的shell脚本,调用execFile,但传参做了处理execFile:原理是直接执行我们传入的file和args,底层调用spawn创建和执行子进程,但通过监听spawn中广播的事件,建立了回调,且一次性将所有的stdout和stderr结果返回spawn:原理是调用internal/child_process,实例化了ChildProcess子进程对象,再调用ChildProcess.prototype.spawn()创建子进程并执行命令,底层调用了child._handle.spawn()执行C++ process_wrap中的spawn方法。执行过程是异步的。执行完后,通过 pipe 进行单向数据通信,通信结束后,子进程发起child._handle.onexit回调,同时 socket 会执行close回调。fork:原理是通过spawn创建子进程和执行命令。使用node执行命令,通过setupchannel创建IPC用于子进程和父进程之间的双向通信
- data/error/exit/close回调的区别
data:主进程读取数据过程中,通过onStreamRead发起回调error:命令执行失败后发起的回调exit:子进程关闭完成后发起的回调close:子进程所有Socket通信端口全部关闭后发起的回调stdout close/stderr close:特定的 PIPE 读取完成后调用onReadableStreamEnd()关闭Socket时发起的回调。
child_process同步源码解析
execSync、 execFileSync 和 spawnSync 执行流程

execSync 、 execFileSync 和 spawnSync 的区别
execSync和execFileSync底层都是调用spawnSync。但是execSync和execFileSync在调用spawnSync之前的参数规范化逻辑不一样:execSync和exec都是调用normalizeExecArgs()execFileSync、spawn及spawnSync调用的是normalizeSpawnArguments()
spawnSync返回的是child_process.spawnSync(opts)中spawn_sync.spawn(options)(C++代码) 执行返回的结果resultexecSync和execFileSync都将spawnSync的执行结果做了同样的处理有错误,直接从主进程
threw错误在
execFileSync('ls -la')找不到文件时result.error
错误日志
child_process.js:642
throw err;
^
Error: spawnSync ls -la ENOENT
at Object.spawnSync (internal/child_process.js:1041:20)
at spawnSync (child_process.js:607:24)
at Object.execFileSync (child_process.js:634:15)
at Object.<anonymous> (/Users/jolly/Desktop/imooc/child_process/index.js:7:19)
at Module._compile (internal/modules/cjs/loader.js:959:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
at Module.load (internal/modules/cjs/loader.js:815:32)
at Function.Module._load (internal/modules/cjs/loader.js:727:14)
at Function.Module.runMain (internal/modules/cjs/loader.js:1047:10)
at internal/main/run_main_module.js:17:11 {
errno: 'ENOENT',
code: 'ENOENT',
syscall: 'spawnSync ls -la',
path: 'ls -la',
spawnargs: [],
error: [Circular],
status: null,
signal: null,
output: null,
pid: 6262,
stdout: null,
stderr: null
}result.stdout和result.stderr中是null
在命令本身错误时:
lss -laresult.error = undefinedresult.stdout中的ArrayBuffer为空result.stderr中ArrayBuffer有值。result.stderr中的信息,将通过process.stderr.write(ret.stderr)打印在主进程的日志中:/bin/sh: lss: command not foundresult.status !== 0,打印result.stderr之后,threw 错误信息
没有错误,返回执行成功的结果输出流
stdout
使用示例
const cp = require('child_process');
const path = require('path');
execSyncconst stdout = cp.execSync('lss -la');
console.log('stdout: ', stdout.toString()); // 有异常不会执行execFileSyncconst stdout = cp.execFileSync(path.resolve(__dirname, 'test.shell'));
console.log('stdout: ', stdout.toString()); // 有异常不会执行spawnSyncconst result = cp.spawnSync(path.resolve(__dirname, 'test.shell'));
if (result.status === 0) { // 没有异常
console.log(result.stdout.toString())
} else {
console.log(result)
}
cluster 工作原理
单个 Node.js 实例运行在单个线程中。 为了充分利用多核系统,有时需要启用一组 Node.js 进程去处理负载任务。
cluster 模块可以创建共享服务器端口的子进程。
工作进程由 child_process.fork() 方法创建,因此它们可以使用 IPC 和父进程通信,从而使各进程交替处理连接服务。
require 加载内置模块解析
判断内置模块
源码
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.module = undefined;
this.exportKeys = undefined;
this.loaded = false;
this.loading = false;
this.canBeRequiredByUsers = !id.startsWith('internal/');
}
// ...
const {
moduleIds,
compileFunction
} = internalBinding('native_module');
NativeModule.map = new Map();
for (let i = 0; i < moduleIds.length; ++i) {
const id = moduleIds[i];
const mod = new NativeModule(id);
NativeModule.map.set(id, mod);
}
moduleIds:内置模块的路径+文件ming。通过C++代码拿到this.canBeRequiredByUsers:是否是内置模块,moduleIds中,internal/文件夹下都是内置模块cluster执行流程
cluter对象,是EventEmitter对象的实例,具有eventemit、on等方法cluster.fork([env])env<object>要添加到进程环境变量的键值对。- 返回:
cluster.worker
cluster.setupMaster([settings])settings<object>- 用于修改默认的
fork行为。 一旦调用,将会按照settings对cluster.settings进行设置。 - 所有的设置只对后来的
.fork()调用有效,对之前的工作进程无影响。
process.execArgv属性返回当·Node.js 进程被启动时,Node.js 特定的命令行选项。 这些选项在
process.argv属性返回的数组中不会出现,并且这些选项中不会包括Node.js的可执行脚本名称或者任何在脚本名称后面出现的选项。 这些选项在创建子进程时是有用的,因为他们包含了与父进程一样的执行环境信息。$ node --harmony script.js --versionprocess.execArgv的结果:['--harmony']process.argv的结果:['/usr/local/bin/node', 'script.js', '--version']setup事件每当
cluster.setupMaster()被调用时触发。work.process是child_process.fork()创建的进程在主进程中,访问
cluster.workers获得工作进程信息。其是一个哈希表,储存了活跃的工作进程对象,使用id作为键名。只能在主进程中访问internal/cluster/child.js规定了工作进程的行为,这里定义了cluster.worker,保存工作进程对象的引用。 在工程进程中可访问,指向当前工作进程。疑问
workerProcess由child_process.fork()创建'internalMessage'事件怎么触发,返回的参数message和handle是什么?- 其
send方法干了啥?
见
child_process异步源码解析文章fork部分,setupChannel(child, ipc)